Skip to content

feat(auth): OAuth 2.1 + PKCE authentication for browser extensions#270

Merged
rosscado merged 6 commits intomainfrom
feat/phase4-pkce-auth
Jan 2, 2026
Merged

feat(auth): OAuth 2.1 + PKCE authentication for browser extensions#270
rosscado merged 6 commits intomainfrom
feat/phase4-pkce-auth

Conversation

@rosscado
Copy link
Copy Markdown
Contributor

@rosscado rosscado commented Jan 1, 2026

Summary

Implements OAuth 2.1 + PKCE (Proof Key for Code Exchange) authentication flow for the SayPi browser extension, replacing the cookie-based authentication on Chrome while maintaining fallback support for Firefox.

Why PKCE?

  • Browser extensions are "public clients" that cannot securely store secrets
  • PKCE provides security through cryptographic challenge/response
  • Industry standard for mobile apps and browser extensions (RFC 7636, OAuth 2.1)
  • Recommended by Google, Microsoft, and Okta for public clients

Changes

New Files

File Purpose
src/auth/PKCEManager.ts PKCE utilities: code verifier/challenge generation, state management
src/auth/OAuthService.ts OAuth flow orchestration using browser.identity.launchWebAuthFlow
test/auth/PKCEManager.spec.ts Unit tests for PKCE utilities (13 tests)

Modified Files

File Changes
wxt.config.ts Added identity permission for Chrome
src/JwtManager.ts OAuth token storage (access + refresh), setOAuthTokens(), refreshWithOAuth(), performRefresh()
src/svc/background.ts AUTHENTICATE_WITH_PKCE message handler, updated alarm handler for OAuth refresh
src/popup/auth-shared.js Try PKCE first, fall back to tab-based flow
src/auth/AuthPromptController.ts Trigger PKCE auth directly from in-page prompts
test/JwtManager.spec.ts Updated tests for new oauthRefreshToken field

Authentication Flow

Chrome (Primary - PKCE)

  1. User clicks "Sign In"
  2. Extension generates PKCE pair (verifier + challenge)
  3. browser.identity.launchWebAuthFlow() opens inline auth popup
  4. User authenticates with OAuth provider
  5. Extension receives authorization code
  6. Code + verifier exchanged for tokens at /api/oauth/token
  7. Tokens stored in JwtManager, refresh scheduled

Firefox (Fallback - Tab-based)

  1. User clicks "Sign In"
  2. Opens auth server in new tab
  3. Cookie-based authentication (existing flow)
  4. cookies.onChanged listener detects auth completion
  5. Return-to-context flow closes auth tab

Token Management

  • Access tokens: Stored in JwtManager, refreshed before expiry
  • Refresh tokens: OAuth refresh tokens with rotation support
  • Exponential backoff: 1min → 2min → 4min on refresh failures
  • Graceful degradation: After 3 failures, clears auth and emits jwt:auth:failed

Test Plan

  • Unit tests for PKCEManager (13 tests)
  • Updated JwtManager tests for new fields
  • All existing tests pass (655 tests)
  • Manual E2E testing with Chrome
  • Manual E2E testing with Firefox (fallback flow)

Dependencies

Requires the SaaS backend OAuth 2.1 + PKCE endpoints from saypi-saas PR #66.

🤖 Generated with Claude Code

rosscado and others added 6 commits January 1, 2026 19:06
Adds secure PKCE-based authentication for browser extensions, replacing
cookie-based auth on Chrome and providing a fallback for Firefox.

Key changes:
- PKCEManager.ts: Generate code verifier/challenge pairs for OAuth
- OAuthService.ts: Orchestrate PKCE flow using browser.identity API
- JwtManager: Store OAuth tokens (access + refresh), support token rotation
- background.ts: Handle AUTHENTICATE_WITH_PKCE message
- auth-shared.js: Try PKCE first, fall back to tab-based flow
- AuthPromptController: Trigger PKCE auth directly from prompts
- Added 'identity' permission for Chrome

Benefits:
- No client secrets needed (public client flow)
- Secure token refresh via OAuth refresh tokens
- Better UX: inline auth popup instead of new tab (Chrome)
- Fallback to existing flow for Firefox

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…error

Vite's modulepreload helper uses `document.createElement` which fails in
service workers. Changed from dynamic import to static import to fix:
"ReferenceError: document is not defined"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add isClearing flag to JwtManager to prevent in-flight token refreshes
from re-authenticating the user during sign-out.

Changes:
- Add isClearing flag that blocks refresh operations during sign-out
- Make clear() async to properly await storage removal
- Add guards to performRefresh(), refresh(), and refreshWithOAuth()
- Add isPKCEAuthInProgress flag to prevent cookie listener interference
- Update background.ts to await clear() calls

Fixes issue where signing out would trigger:
1. clear() sets token to null
2. In-flight refresh completes and writes new token
3. User appears re-authenticated immediately

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…rror

Follow-up to 190c23b - apply the same static import fix to PKCEManager
that was applied to OAuthService.

Changes:
- Replace dynamic imports with static import for wxt/browser
- Disable modulePreload in wxt.config.ts to prevent window-dependent
  polyfill injection into service worker bundles
- Add tests for PKCE state storage functions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive spec document covering:
- Why PKCE is needed for browser extensions (public clients)
- Chrome Identity API integration details
- Required server endpoints and behavior
- Redirect URI registration requirements
- Acceptance criteria for implementation

This document served as the implementation guide for the PKCE auth
flow and documents known issues encountered during development.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Also update PKCE spec with implementation status.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@rosscado rosscado merged commit d373ec5 into main Jan 2, 2026
1 check passed
@rosscado rosscado deleted the feat/phase4-pkce-auth branch January 2, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant